namespace IBS.Model

open System
open System.IO
open System.Net
//open System.Linq
open System.Xml
open System.Xml.Linq

module ExternalDataService =
    // types of data providers
    type Provider =
        | NBRB   = 0
        | Yahoo  = 1
        | Google = 2
        | ECB    = 3
        | FED    = 4
    
    // rates results
    type ExchangeRate(currency, rate, date) =
        member x.Currency = currency
        member x.Rate = rate
        member x.Date = date
        new(currency, rate) = ExchangeRate(currency, rate, DateTime.Now.Date)
       
    // several functions for data processing
    let private tagRegex = Text.RegularExpressions.Regex "<b>([^<]*)</b>"
    let private numRegex = Text.RegularExpressions.Regex "rhs: \"(\d|\.)*"
    
    let private parse x = Decimal.Parse x
    let private parseReg (reg: Text.RegularExpressions.Regex) (gr: int) x = 
        let m = reg.Match x
        if m.Success then 
            let v = m.Groups.[gr].Value
            match v.IndexOf "\"" with
            | -1  -> Decimal.Parse v
            | ind -> Decimal.Parse v.[ind+1..]
        else 0M
       
    let private fetch (url : Uri) = 
        let req = WebRequest.Create url :?> HttpWebRequest
        use stream = req.GetResponse().GetResponseStream()
        use reader = new StreamReader (stream)
        reader.ReadToEnd()

    let private reformat (response:string) = 
        let split (mark:char) (data:string) = data.Split mark |> Array.toList
        response |> split '\n' 
        |> List.filter (fun f -> f <> "") 
        |> List.map (split ',') 
        
    let private getRequest uri = (fetch >> reformat) uri 
    
    // string to XName conversion
    let inline xn str = XName.Get str
    // get attribute value
    let inline xa attr (x: XElement) = x.Attribute(xn attr).Value
    // get element
    let inline xe name (x: XElement) = x.Element(xn name).Value
    
    // find xml XElemnt nodes by given name
    let private findXmlNodes data node =
        XElement.Parse(data).DescendantNodes() 
        |> Seq.map (function 
            | :? XElement as x when string x.Name = node -> x
            | _ -> null)
        |> Seq.filter ((<>) null)  
    
    // convert rates from one currency to another through base currency
    let private convertRates cfrom cto baseCcy date (rates: Map<_,_>) =
        Array.map (fun c ->  
                if c = cto then Some (c, 1M)
                elif c = baseCcy then
                    Option.bind (fun r -> Some (cto, r)) (rates.TryFind cto)
                elif cto = baseCcy then
                    Option.bind (fun r -> if r = 0M then None else Some (c, 1M / r)) (rates.TryFind c)
                else
                    match rates.TryFind c, rates.TryFind cto with
                    | Some 0M, _  -> None
                    | Some r1, Some r2 -> Some (c, r2 / r1)
                    | _ -> None) cfrom
        |> Array.filter Option.isSome
        |> Array.map (fun (Some (c, r)) -> ExchangeRate(c, r, date))      

    // fetch historical rates
    let fetchRates cfrom cto (date: DateTime) = function
        // FED doesn't necessary return the current date data, but can be used for historical rates
        | Provider.FED ->
            let datestr = date.ToString "MM/dd/yyyy"
            let uri = sprintf "http://www.federalreserve.gov/datadownload/Output.aspx?rel=H10&series=a6a0113179fdeb9c6fbcdc18575ec09c&lastObs=&from=%s&to=%s&filetype=csv&label=include&layout=seriescolumn" datestr datestr
            let feddata = getRequest (Uri uri) |> List.toArray
            if feddata.Length < 7 then [||]
            else 
                //skip headers and USD
                Seq.zip feddata.[3] feddata.[6]
                |> Seq.skip 1
                |> Seq.skipWhile (fun (x, _) -> x = "\"USD\"")
                |> Seq.map (fun (c, r) -> c.[1..3], parse r)
                |> Map.ofSeq
                |> convertRates cfrom cto "USD" date
        // NBRB - default provider
        | _ ->
            let service = "http://www.nbrb.by/Services/ExRates.asmx"
            let httpRequest = WebRequest.Create service :?> HttpWebRequest
            httpRequest.Method <- "POST"
            httpRequest.ContentType <- "text/xml; charset=utf-8"
            httpRequest.Headers.Add "SOAPAction: \"http://www.nbrb.by/ExRatesDaily\""
            httpRequest.ProtocolVersion <- HttpVersion.Version11
            httpRequest.Credentials <- CredentialCache.DefaultCredentials
            
            let requestStream = httpRequest.GetRequestStream()
            let streamWriter = new StreamWriter(requestStream, Text.Encoding.ASCII)
            let soapRequest  = "<soap:Envelope xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"" + 
                               " xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\">" +
                               "<soap:Body>" +
                               "<ExRatesDaily xmlns=\"http://www.nbrb.by/\"><onDate>" + 
                               date.ToString "yyyy-MM-dd" +
                               "</onDate></ExRatesDaily></soap:Body></soap:Envelope>"
            streamWriter.Write (soapRequest.ToString())
            streamWriter.Close()
            let response = httpRequest.GetResponse() :?> HttpWebResponse
            let reader = new StreamReader(response.GetResponseStream())
            findXmlNodes (reader.ReadToEnd()) "DailyExRatesOnDate"
            |> Seq.map (fun x -> xe "Cur_Abbreviation" x, parse (xe "Cur_OfficialRate" x))
            |> Map.ofSeq
            |> convertRates cfrom cto "BYR" date
            
    // load the current date rates
    let currentRates cfrom cto = function
        | Provider.Google ->
            Array.map (fun c -> 
                let result = Uri (sprintf "http://www.google.com/ig/calculator?hl=en&q=%s=?%s" c cto) |> fetch
                ExchangeRate (c, parseReg numRegex 0 result)) cfrom
        | Provider.ECB ->
            let result = fetch (Uri "http://www.ecb.int/stats/eurofxref/eurofxref-daily.xml")
            findXmlNodes result "Cube"
            |> Seq.filter (fun x -> x.Attribute(xn "currency") <> null)
            |> Seq.map (fun x -> xa "currency" x, xa "rate" x |> parse)
            |> Map.ofSeq
            |> convertRates cfrom cto "EUR" DateTime.Now.Date                   
        | Provider.Yahoo -> 
            let symbols = 
              Array.filter (fun c -> not (String.IsNullOrWhiteSpace c) && c.Length = 3) cfrom
              |> Array.map (fun c -> "s=" + c + cto + "=X") 
            Uri (sprintf "http://download.finance.yahoo.com/d/quotes.csv?%s&f=n11d1t1" (String.Join("&", symbols)))
            |> getRequest
            |> List.filter (fun x -> x.Length > 2)
            |> List.map (fun (t::c::_) -> ExchangeRate (c.[..6], parseReg tagRegex 1 t))
            |> List.toArray
        | provider -> fetchRates cfrom cto DateTime.Now.Date provider
            

      